feat(rig): provision .claude/worktrees ignore via global core.excludesfile (supersedes #23)#29
Conversation
…sfile Rebuild #23's gitignore feature under the CTO-chosen GLOBAL design: rig owns ONE marker-delimited block in git's global core.excludesfile so the harness's throwaway **/.claude/worktrees/ is ignored in EVERY repo on the machine, with zero per-repo commits and no per-repo `rig apply`. Wired like the git-hooks dispatcher (a `git config --global` setting + a managed file), in the GLOBAL rig layer. Target resolution at apply time: honor an existing core.excludesfile; when unset, set it to ~/.config/git/ignore AND write the block there (so a clean machine is fully provisioned by `rig init` alone). Reuses #23's marker reconcile machine (create/update/ok/conflict/ io_error), retargeted, plus: - strict idempotency: a re-apply is a byte-identical no-op; - dedup of the managed region: several duplicated blocks collapse to one, preserving user content BETWEEN blocks (splice per marker-pair, not first-begin..last-end); - drift in the GLOBAL section flags a missing/divergent/duplicated block and an unset core.excludesfile rig would set. Default ON at plan level (an absent `gitignore` key still provisions), so the GLOBAL block is NOT scaffolded into committed repo rig.yaml. The canonical block text (markers + fixed comment + entry) is byte-identical to what a provisioned machine already has, making re-apply zero-churn. Review: ran multi-model `review` on this diff; addressed findings (interleaved user-line preservation, XDG isolation in tests, scaffold cleanup, doc fixes). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- tests/test_global_excludes.py: target resolution both ways (core.excludesfile set vs unset, with injected git-config seams so no test runs real `git config --global`), every reconcile state, STRICT byte-identical re-apply, the dedup-of-managed-region collapse (incl. preserving a user line between blocks), CRLF/trailing-blank/no-final-newline preservation, drift parity, disabled-but- installed leftover scan, and a byte-stable canonical-block regression pin. - conftest: autouse fixtures isolate HOME + XDG_CONFIG_HOME and stub the git-config read/write seams suite-wide, so a full-plan e2e test can never touch the real ~/.gitignore or real global git config. - smoke.sh: HOME-isolated leg exercises the real init/apply flow — asserts core.excludesfile is set to the XDG default, the block is written, and the block is byte-stable (markers=2) across a re-apply. Also makes the mcp leg conditional on the carrier and prefers `uv run pytest`. - docs/config-schema.md: the `gitignore` (global core.excludesfile) section and validation note. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
4a1b1f5 to
6c54306
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6c54306724
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if xdg and (path_str == "~/.config" or path_str.startswith("~/.config/")): | ||
| path_str = xdg + path_str[len("~/.config"):] |
There was a problem hiding this comment.
Resolve configured ~/.config paths the same way Git does
When $XDG_CONFIG_HOME is set and core.excludesfile is unset (or already ~/.config/git/ignore), this rewrite makes rig write the managed block to $XDG_CONFIG_HOME/git/ignore, while _set_git_global stores ~/.config/git/ignore; Git then expands that configured pathname to $HOME/.config/git/ignore, not XDG (verified with git config --type=path; the git-config docs say core.excludesFile defaults to $XDG_CONFIG_HOME/git/ignore only when the option is unset: https://git-scm.com/docs/git-config#Documentation/git-config.txt-coreexcludesFile). In that environment apply/status report success, but Git does not ignore .claude/worktrees/.
Useful? React with 👍 / 👎.
…ixture #27's clean-sample leg enumerates every default-ON category and disables it to assert zero false drift. tg_ctl (added by this PR) is default-ON, so its provision_tg_ctl action made the clean sample report drift (exit 3). Mirror how #29 added 'gitignore: {enabled: false}' and disable tg_ctl too.
* feat(rig): add tg_ctl config block + pure plist planning Add the tg_ctl config block (validate + plan) and the pure, effect-free TgCtlPlan that renders the ai.hyperide.tg-ctl.plist LaunchAgent XML byte-exact to the working hand-created file (sort_keys=False preserves the insertion order so a re-apply is a true no-op). Default-on, per-machine (GLOBAL layer), macOS-only. Mirrors the tmux block's schema style. boot:null and label:null resolve to their defaults (not bool(None)=False / str(None)="None"). Reviewed via multi-model `review`; findings addressed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(rig): provision + reconcile tg-ctl boot LaunchAgent Runner: _do_provision_tg_ctl writes the byte-exact plist, backs up a differing prior, ensures the log dir, tears down the stale predecessor (com.ultra.codex-tg-bot: bootout + timestamped backup + remove), and (re)loads via launchctl bootout/bootstrap in the gui/<uid> domain. A re-apply against the already-correct loaded plist is a skipped no-op. RIG_TG_CTL_DRY_RUN writes the plist but skips every live/destructive mutation (launchctl AND the stale teardown) so tests/smoke never touch the real launchd domain. Drift: _check_tg_ctl flags missing / divergent / written-but-not-loaded, a leftover plist when boot:false, and the stale predecessor (extra). CLI: GLOBAL status line shows installed / drifted / disabled / unsupported (off-darwin), resolved through the shared plan builder. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(rig): tg-ctl unit suite + HOME-isolated smoke leg test_tg_ctl.py mirrors test_tmux.py: config validation, byte-exact plist render (incl. against the live machine plist when present, read-only), create/idempotent/conflict/dry-run states, stale-predecessor teardown, drift (missing/modified/extra/not-loaded), status states, and the boot:null / label:null / dry-run-no-stale-removal / off-darwin regressions. conftest neutralizes the default-on tg_ctl provisioner + drift check and stubs the gui-domain launchctl seams suite-wide (dedicated tests restore the real ones with their own HOME-isolated tmp dirs); no test ever touches the real ~/Library/LaunchAgents or runs real launchctl. smoke.sh gains a focused, HOME-isolated, RIG_TG_CTL_DRY_RUN tg-ctl leg and prefers `uv run --extra test pytest`. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(rig): document the tg_ctl config block docs/config-schema.md: the tg_ctl section (keys, defaults, the byte-exact no-op contract, gui-domain (re)load, stale-predecessor teardown, drift, the RIG_TG_CTL_DRY_RUN seam, and the enabled:false vs boot:false distinction) + the validation paragraph. AGENTS.md: refine the "never mutate a LIVE service" rule — the stateless background daemons (models cron, tg_ctl) are the documented (re)load exceptions. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(rig): disable tg_ctl in the error-system-v2 clean-sample smoke fixture #27's clean-sample leg enumerates every default-ON category and disables it to assert zero false drift. tg_ctl (added by this PR) is default-ON, so its provision_tg_ctl action made the clean sample report drift (exit 3). Mirror how #29 added 'gitignore: {enabled: false}' and disable tg_ctl too. --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
What & why
Rebuilds #23's gitignore feature under the CTO-chosen GLOBAL design.
#23 wrote a managed block into each repo's committed
.gitignore(per-repo, needs arig applyper repo). The CTO rejected that and chose a global git excludesfile: rig manages ONE marker block in git's globalcore.excludesfile, so**/.claude/worktrees/is ignored in every repo on the machine — zero per-repo commits, no per-reporig apply.This reuses #23's tested marker-reconcile machine (
create/update/ok/conflict/io_error, the offset-based splice, drift parity, thegitignore:schema), retargeted to the global file.Design
Global config, wired like the git-hooks
dispatcher— agit config --globalsetting plus a managed file, living in the GLOBAL rig layer. Default ON at plan level (an absentgitignorekey still provisions), so the global block is not scaffolded into committed reporig.yaml.Target resolution (at apply time):
core.excludesfilealready set (this machine:~/.gitignore) → manage the block in that file, leave git config alone (respect the user's choice).core.excludesfileunset → set it to the XDG default~/.config/git/ignoreand write the block there. So on a clean machinerig initdoes everything itself (set the config if absent + write the block).gitignore.excludesfile:override → force a specific file.Managed block — fenced by
# >>> rig-managed (do not edit) >>>/# <<< … <<<, a fixed explanatory comment, then the entries (default["**/.claude/worktrees/"], configurable). Only the bytes between the markers are ever touched; everything else (CRLF, trailing blanks, no-final-newline) is preserved verbatim.Strict idempotency — a re-apply is a byte-identical no-op. If a prior non-idempotent tool appended the block more than once, rig collapses the managed region to one correct block, preserving any user line that sits between duplicated blocks (splice per marker-pair, not first-begin..last-end).
.serena/is deliberately not ignored (committed shared memory).Drift —
rig statusflags a missing/divergent/duplicated block, and an unsetcore.excludesfilerig would set, in the global section.rig applyreconciles.Tests / smoke
uv run pytest tests/→ 509 passed.bash tests/smoke.sh→ exit 0 (HOME-isolated leg assertscore.excludesfileset to the XDG default, the block written, and markers=2 byte-stable across re-apply; pytest leg green).git config --globalor writes the real~/.gitignore: the git-config read/write seams are stubbed suite-wide and HOME/XDG are isolated.~/.gitignoreblock:resolve_global_excludes(...)→ok(no rewrite).reviewon the diff; addressed findings (interleaved-user-line preservation, XDG test isolation, removed the global block from the committed repo scaffold, doctor→status doc fix, CRLF create-path test, SYNC note on the duplicated path-expander).Supersedes #23
#23 should be closed as superseded — leaving that to the CTO (not closing it here).
Notes
origin/mainhas not moved since branch-off (error-system feat(errors): structured what/why/fix errors + stable exit codes (error-system v2) #27 and the tmux-v2 PR have not landed yet, so no rebase conflicts). Will re-run tests+smoke after those land if they touch these files.🤖 Generated with Claude Code